﻿using System;
using System.Linq;
using System.Xml.Serialization;
using System.IO;
using System.Threading;
using System.Text;
using System.Security.Cryptography;
using System.IO.IsolatedStorage;
using System.Runtime.Serialization;
using PasswordProvider.ObjectModel.Properties;


namespace PasswordProvider.ObjectModel
{
    /// <summary>
    /// File backup modes
    /// </summary>
    public enum FileBackupMode
    {
        /// <summary>
        /// Automatic backup is never performed.
        /// </summary>
        NeverBackup,
        /// <summary>
        /// Backup is performed with every save, if there is a valid backup file location.
        /// </summary>
        BackupEveryChange
        //BackupOnAnIntervalOfDays
    }

    /// <summary>
    /// FileManager encapsulates file operations for the password provider data object, including serialization,
    /// deserialization of xml, management of file paths, and management of the backup system. 
    /// </summary>
    internal static class FileHelper
    {
        readonly static byte[] _encryptedFileHeaderBytes = BitConverter.GetBytes(110460);
        
       
        #region Public Methods

        /// <summary>
        /// Opens the file at the specificed location.
        /// </summary>
        internal static PasswordProviderData Open(string path, ProtectedString password, Cipher cipher)
        {
            return DeSerialize(path, password, cipher);
        }

        internal static void Save()
        {
            if(PasswordProviderData.Current != null)
                Save(PasswordProviderData.Current);
        }

        internal static void Save(PasswordProviderData data)
        {            
            SaveInternal(data, Settings.Default.ActiveFilePath);

            if (Settings.Default.FileBackupMode == FileBackupMode.BackupEveryChange)
            {
                BackupIfRequired(data);
            }
        }

        internal static void Save(PasswordProviderData data, string path)
        {
            SaveInternal(data, path);
        }

        internal static void Backup(PasswordProviderData data)
        {
            string path = Settings.Default.BackupFilePath;
            if (!String.IsNullOrEmpty(path) && Directory.Exists(Path.GetDirectoryName(path)))
            {
                SaveInternal(data, path);
            }
        }
        
        #endregion

        #region Private Properties and Methods

        private static void BackupIfRequired(PasswordProviderData data)
        {
            if (Settings.Default.FileBackupMode == FileBackupMode.BackupEveryChange)
            {
                Backup(data);
            }
        }
        


        /// <summary>
        /// Saves the data as a file at the specified location.
        /// </summary>
        private static void SaveInternal(PasswordProviderData data, string path)
        {
            EnsureDirectoryExists(Path.GetDirectoryName(path));

            if (Settings.Default.EncryptDataFile)
            {
                SerializeEncrypted(data, path);                
            }
            else
            {
                Serialize(data, path);
            }

        }

        // Serializes the password provider data to the file specified.
        private static void Serialize(PasswordProviderData sites, string path)
        {
            if (sites == null) throw new ArgumentNullException("sites");

            DataContractSerializer serializer = new DataContractSerializer(sites.GetType(), null, System.Int32.MaxValue, false, true, null);
            using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                serializer.WriteObject(fileStream, sites);
            }
        }

        /// <summary>
        /// Serialize the data to an encrypted file.
        /// </summary>
        private static void SerializeEncrypted(PasswordProviderData sites, string path)
        {
            if (sites == null) throw new ArgumentNullException("sites");

            // Open the file for writing.
            using (FileStream fileStream = new FileStream(path, FileMode.Create, FileAccess.Write))
            {
                fileStream.Write(_encryptedFileHeaderBytes, 0, _encryptedFileHeaderBytes.Length); // Write a _encryptedFileHeader to identify the file.
                fileStream.Write(BitConverter.GetBytes(sites.Cipher.MasterKeyHash.Length), 0, sizeof(Int32)); // Write the size of the hash
                fileStream.Write(sites.Cipher.MasterKeyHash, 0, sites.Cipher.MasterKeyHash.Length); // Write the hash
                fileStream.Write(BitConverter.GetBytes(sites.Cipher.MasterKeySalt.Length), 0, sizeof(Int32)); // Write the size of the salt
                fileStream.Write(sites.Cipher.MasterKeySalt, 0, sites.Cipher.MasterKeySalt.Length); // Write the salt.
                fileStream.Write(BitConverter.GetBytes(sites.Cipher.MasterKeyGenerationIterations), 0, sizeof(Int32)); // write the iteration count

                using (RijndaelManaged cypher = sites.Cipher.CreateRijndaelManagedForFile())
                {
                    // Write the IV and its size from the newly created cypher.
                    fileStream.Write(BitConverter.GetBytes(cypher.IV.Length), 0, sizeof(int));
                    fileStream.Write(cypher.IV, 0, cypher.IV.Length);

                    // serialize the xml to the crypto, which sends its output to the file stream.
                    DataContractSerializer serializer = new DataContractSerializer(sites.GetType(), null, System.Int32.MaxValue, false, true, null);                    
                    using (CryptoStream cryptoStream = new CryptoStream(fileStream, cypher.CreateEncryptor(), CryptoStreamMode.Write))
                    {
                        serializer.WriteObject(cryptoStream, sites);
                    }
                }
            }
        }


        /// <summary>
        /// Determine if the file is encrypted in Password Provider format.
        /// </summary>
        private static bool IsFileEncrypted(FileStream fileStream)
        {
            byte[] headerBytes = new byte[_encryptedFileHeaderBytes.Length];
            if (fileStream.Length > _encryptedFileHeaderBytes.Length)
            {
                fileStream.Read(headerBytes, 0, headerBytes.Length);
                fileStream.Position = 0; 
                return headerBytes.SequenceEqual(_encryptedFileHeaderBytes);
            }            
            return false;
        }

        // Deserializes content of file at path into a ProtectedCollection instance and returns the instance. 
        // If content is not valid xml, CorruptFileException is thrown.
        // If content is valid xml, but not password provider data, InvalidFileTypeException is thrown.
        private static PasswordProviderData DeSerialize(string path, ProtectedString password, Cipher cipher)
        {
            if (!File.Exists(path))
                throw new InvalidOperationException("No file at specified path");

            PasswordProviderData collection = null; 

            using (FileStream fileStream = new FileStream(path, FileMode.Open, FileAccess.Read))
            {
                if (fileStream.Length > 0)
                {
                    try
                    {
                        if(IsFileEncrypted(fileStream))
                            collection = DeSerializeStream(fileStream, password, cipher);
                        else
                            collection = DeSerializeStream(fileStream);
                    }
                    catch (CryptographicException e)
                    {
                        throw new CorruptFileException(path, e);
                    }
                    catch (InvalidOperationException e)
                    {
                        throw new CorruptFileException(path, e);
                    }
                }
                else // file is empty.
                    throw new CorruptFileException("File is empty: " + path);
            }

            return collection;
            
        }

        /// <summary>
        /// Deserialize unencrypted file from stream.
        /// </summary>
        private static PasswordProviderData DeSerializeStream(FileStream fileStream)
        {
            DataContractSerializer serializer = new DataContractSerializer(typeof(PasswordProviderData));
            return (PasswordProviderData)serializer.ReadObject(fileStream);            
        }

        /// <summary>
        /// Read encrypted file, deserialize into ProtectedSiteCollection.
        /// </summary>
        private static PasswordProviderData DeSerializeStream(FileStream fileStream, ProtectedString password, Cipher cipher)
        {
            
                // First read the _encryptedFileHeader and verify file type.
                byte[] headerBytes = new byte[_encryptedFileHeaderBytes.Length];
                fileStream.Read(headerBytes, 0, headerBytes.Length);
                if (!headerBytes.SequenceEqual(_encryptedFileHeaderBytes))
                    throw new InvalidOperationException("Encrypted file header bytes are invalid");

                byte[] intValue = new byte[sizeof(Int32)];
                int size;

                // Next obtain the size of hash then hash.
                fileStream.Read(intValue, 0, sizeof(Int32));
                size = BitConverter.ToInt32(intValue, 0);
                byte[] hash = new byte[size];
                fileStream.Read(hash, 0, size);

                // Next obtain the size of Salt and Salt
                fileStream.Read(intValue, 0, sizeof(Int32));
                size = BitConverter.ToInt32(intValue, 0);
                byte[] salt = new byte[size];
                fileStream.Read(salt, 0, size);

                // Next obtain the number of iterations.
                fileStream.Read(intValue, 0, sizeof(Int32));
                int iterations = BitConverter.ToInt32(intValue, 0);

                // initialize and authenticate the file Cypher
                cipher.Initialize(hash, salt, iterations);
                // if the password can't be authenticated, return null.
                if (!cipher.Authenticate(password))
                    return null;

                using (RijndaelManaged rijndael = cipher.CreateRijndaelManagedForFile())
                {
                    // First obtain the IV for the cypher.
                    fileStream.Read(intValue, 0, sizeof(Int32));
                    size = BitConverter.ToInt32(intValue, 0);
                    byte[] IV = new byte[size];
                    fileStream.Read(IV, 0, size);
                    rijndael.IV = IV;

                    // Open the crypto stream for reading from the file stream, passing the decryptor from the cypher.
                    using (CryptoStream cs = new CryptoStream(fileStream, rijndael.CreateDecryptor(), CryptoStreamMode.Read))
                    {
                        DataContractSerializer serializer = new DataContractSerializer(typeof(PasswordProviderData));
                        PasswordProviderData collection = (PasswordProviderData)serializer.ReadObject(cs);
                        return collection;
                    }
                }
            

        }

        // If the directory does not exist, an attempt is made to create it.
        // Returns false if unable to create the directory.
        private static void EnsureDirectoryExists(string directoryPath)
        {
            if (!Directory.Exists(directoryPath))
            {
                Directory.CreateDirectory(directoryPath);                
            }
        }
        #endregion

    }

}
